const EventEmitter = require('events').EventEmitter
const SerialPort = require('serialport')
var ModbusRTUEx = require('./modbus_zigbee')

const MIN_DATA_LENGTH = 7
const EXCEPTION_LENGTH = 6

/**
 * Simulate a modbus-RTU port using buffered serial connection over zigbee transport
 * coupled with ModbusRTUEx module
 */

class RTUBufferdPortEx extends EventEmitter {
  constructor (path, options) {
    super()

    // options
    if (typeof (options) === 'undefined') {
      options = {}
    }

    // disable auto open, as we handle the open
    options.autoOpen = false

    // internal buffered
    this._buffer = Buffer.alloc(0)
    this._id = 0
    this._cmd = 0
    this._length = 0

    // create the SerialPort
    this._client = new SerialPort(path, options)

    this._client.on('data', (data) => this._onData(data))
  }

  // ///////////////////////////////////////////////////////////
  // Public Methods
  // ///////////////////////////////////////////////////////////

  /**
   * delegate the serialport open function
   */
  open (callback) {
    this._client.open(callback)
  }

  /**
   * delegate the serialport close function
   */
  close (callback) {
    this._client.close(callback)
  }

  /**
   * delegate the serialport isOpen function
   */
  isOpen () {
    return this._client.isOpen()
  }

  /**
   * set target id for received response msg
   * this method should be called befor write() method
   * @param id {number} id for received response msg
   */
  setId (id) {
    this._targetId = id
  }
  /**
   * Send data to a modbus salve using serialport
   * the payload is tranported over zigbee
   * @param data {Buffer} payload is transfered over zigbee
   */
  write (data) {
    this._except_data = false
    if (data.length === EXCEPTION_LENGTH) {
      // local cmd
      this._length = 5
      this._except_data = true
      this._client.write(data)
      return
    }
    if (data.length < MIN_DATA_LENGTH) {
      this.emit('error', 'data length error')
      return
    }

    // extract command info
    this._cmd = data[2]

    let length
    // calculate expected answer length
    switch (this._cmd) {
      case 1:
      case 2:
        length = data.readUInt16BE(5)
        this._length = 4 + parseInt((length - 1) / 8 + 1) + 2
        break
      case 3:
      case 4:
        length = data.readUInt16BE(5)
        this._length = 4 + 2 * length + 2
        break
      case 5:
      case 6:
      case 15:
      case 16:
        this._length = 7 + 2
        break
      default:
        this.emit('error', `unsupported command code: ${this._cmd}`)
        this._length = 0
        break
    }

    this._client.write(data)
  }
  // ///////////////////////////////////////////////////////////
  // Private Methods
  // ///////////////////////////////////////////////////////////

  /**
   * handle serialport 'data' event
   * @param data {Buffer} the received buffer from serialport
   * @private
   */
  _onData (data) {
    this._buffer = Buffer.concat([this._buffer, data])

    // check if buffer include a complete modbus answer
    const expectedLength = this._length
    const bufferLength = this._buffer.length
    if (this._except_data) {
      if (bufferLength === expectedLength) {
        this._emitData(0, expectedLength)
      }
      return
    }
    if (expectedLength < MIN_DATA_LENGTH || bufferLength < EXCEPTION_LENGTH) {
      return
    }

    // loop and check length-sized buffer chunks
    const maxOffset = bufferLength - EXCEPTION_LENGTH
    for (let i = 0; i <= maxOffset; i++) {
      let rcvId = this._buffer.readUInt16BE(i)
      let functionCode = this._buffer.readInt8(i + 2)

      if (rcvId !== this._targetId) {
        continue
      }

      if (functionCode === this._cmd && i + expectedLength <= bufferLength) {
        this._emitData(i, expectedLength)
        return
      }
      if (functionCode === (0x80 | this._cmd) && i + EXCEPTION_LENGTH <= bufferLength) {
        this._emitData(i, EXCEPTION_LENGTH)
        return
      }
    }
  }

  /**
    * Emit the received response, cut the buffer and reset the internal vars.
    * @param {number} start the start index of the response within the buffer
    * @param {number} length the length of the response
    * @private
 */
  _emitData (start, length) {
    this.emit('data', this._buffer.slice(start, start + length))
    this._buffer = this._buffer.slice(start + length)
  }
}

/**
 * Connect to a communication port, using Bufferd Serial port over zigbee.
 *
 * @param path {string} the path to the Serial Port - required.
 * @param useZigbee {boolean} indicates modbus over zigbee is used
 * @param options{Object}  - the serial port options - optional.
 * @param next{Function}  the function to call next.
 */
ModbusRTUEx.prototype.connectRTUBufferedEx = function (path, useZigbee, options, next) {
  // check if we have options
  if (typeof (next) === 'undefined' && typeof (options) === 'function') {
    next = options
    options = {}
  }
  if (useZigbee) {
    this._port = new RTUBufferdPortEx(path, options)
  } else {
    // create the SerialPort
    var SerialPort = require('modbus-serial/ports/rtubufferedport')
    this._port = new SerialPort(path, options)
  }
  // open and call next
  this.open(useZigbee, next)
}
module.exports = RTUBufferdPortEx
